package scoping;


import ic.ast.Node;
import ic.ast.decl.DeclClass;
import ic.ast.decl.DeclMethod;
import ic.ast.decl.Program;
import ic.ast.decl.Type;
import ic.ast.stmt.LocalVariable;
import ic.ast.stmt.Statement;
import ic.ast.stmt.StmtBlock;
import ic.ast.stmt.StmtBreak;
import ic.ast.stmt.StmtContinue;
import ic.ast.stmt.StmtIf;
import ic.ast.stmt.StmtWhile;

import java.util.ArrayList;
import java.util.List;

import pars.Tree;
import scoping.blocks.ClassScope;
import scoping.blocks.GlobalScope;
import scoping.blocks.MethodScope;
import scoping.blocks.ScopeBlock;
import scoping.blocks.StmtBlockScope;
import scoping.helpers.FaultyType;
import scoping.helpers.ScopeException;
import scoping.helpers.SemanticCheckerResponse;

public class Checker {
	
	Verifier verifier;
	
	public SemanticCheckerResponse check(Tree ast,boolean isHandleMain){
		ScopeTree mainScopeTree = null;
		try
		{
			mainScopeTree = buildScopeTree(ast, null,isHandleMain);
		}catch (ScopeException se){
			String err = se.getLine() + ": " + se.getMessage();
			return new SemanticCheckerResponse(null, 
					err, false);
		}
		verifier = new Verifier(mainScopeTree);
		Type response = checkValidity(ast, verifier);
		String errMessage = response instanceof FaultyType ? ((FaultyType)response).getLine() + ": " + ((FaultyType)response).getMessage() : "";
		return new SemanticCheckerResponse(mainScopeTree, errMessage, !(response instanceof FaultyType));
	}
	
	public Type getType (Node astNode){
		return (Type) astNode.accept(verifier);
	}

	private ScopeTree buildScopeTree(Tree ast, ScopeTree parent,boolean isHandleMain) throws ScopeException{
		ScopeBlock scope = null;
		ScopeTree scopeTree = null;
		if (ast.root instanceof Program){
			for (Tree subTree:ast.subtrees){
				if (subTree.root instanceof DeclClass && ((DeclClass)subTree.root).getName().equals("Library")){
					scope = createBlock((Program)ast.root, (DeclClass)subTree.root);
					break;
				}
			}
			scope = scope == null ? createBlock((Program)ast.root) : scope;
		} else if (ast.root instanceof DeclClass) {
			scope = createBlock((DeclClass)ast.root,isHandleMain);
			DeclClass declClass = (DeclClass)ast.root;
			if (declClass.hasSuperClass()){
				for (ScopeTree tree:parent.getChildren()){
					ClassScope potentialParent = null;
					if (tree.getBlock() instanceof ClassScope)
					{
						potentialParent = findBaseClassScope(((ClassScope)tree.getBlock()), declClass);
						if (potentialParent != null){
							potentialParent.addDerivingClass(declClass);
							parent = potentialParent.getClassDeclaration().getScope();
							scopeTree = new ScopeTree(parent, scope);
							parent.getChildren().add(scopeTree);
							break;
						}
					}				
				}
			}
		} else if (ast.root instanceof DeclMethod) {
			scope = createBlock((DeclMethod)ast.root);
		} else if (ast.root instanceof StmtBlock) {
			scope = createBlock((StmtBlock)ast.root);
		}
		
		if (scope == null){
			scopeTree = parent;
		} else {
			scopeTree = scopeTree == null ? new ScopeTree(parent, scope) : scopeTree;
		}
		
		((Node)ast.root).setScope(scopeTree);
		if (scopeTree.getBlock() instanceof ClassScope){
			// Ex4b, population of offsets.
			((ClassScope)scopeTree.getBlock()).init();
		}
		
		for (Tree subTree:ast.subtrees){
			ScopeTree childScope = buildScopeTree(subTree, scopeTree,isHandleMain);
			if (childScope != null /*parsed child as a scope*/ && childScope != scopeTree && childScope.getParent() == scopeTree){
					/*in the case that childScope == scopeTree the actual child wasn't a scope and was replaced by its parent to append the grandchildren.
					 * second condition is for inheritance (globalScope is not configured as parent scope of a classScope that has a super class
					 * and therefore we do not add it as a child to the global scope. */
				scopeTree.addChild(childScope);
				/* Mark stmt scope blocks as while*/
				if (childScope.getBlock() instanceof StmtBlockScope) {
					StmtBlockScope stmtBlockScope = ((StmtBlockScope)childScope.getBlock());
					// If the stmt block scope was previosly configured, do not reset it to false.
					stmtBlockScope.setIsWhile(ast.root instanceof StmtWhile || (stmtBlockScope.getIsWhile() && stmtBlockScope.getIsOneLiner()));				
				}
			} else if (ast.root instanceof StmtWhile &&
					(subTree.root instanceof StmtBreak || subTree.root instanceof StmtContinue)){
				// This is a really ugly hack.
				// Create one liner stmt block scope so that the semantic checking would not report 
				// these break or continue statements as out of while
				List<Statement> stmtList = new ArrayList<Statement>();
				Statement brkContinueStmt = (Statement)subTree.root;
				stmtList.add(brkContinueStmt);
				scope = createBlock(new StmtBlock(brkContinueStmt.getLine(), stmtList), true);
				((StmtBlockScope)scope).setIsWhile(true);
				scopeTree = new ScopeTree(parent, scope);
				brkContinueStmt.setScope(scopeTree);
			}
		}
		return scopeTree;
	}
	
	private ClassScope findBaseClassScope(ClassScope candidate, DeclClass child){
		ClassScope classScope = null;
		if (candidate.getClassDeclaration().getName().equals(child.getSuperClassName())){
			return candidate;
		}
		if (candidate.hasDerivingClasses()){
			for (DeclClass newCandidateClass:candidate.getDerivingClasses()){
				classScope = findBaseClassScope((ClassScope)newCandidateClass.getScope().getBlock(), child);
				if (classScope != null){
					return classScope;
				}
			}
		}
			
		return classScope;
	}

	private Type checkValidity(Tree ast, Verifier verifier){
		for (Tree astChild:ast.subtrees){
			Type response = checkValidity(astChild, verifier);
			if (response instanceof FaultyType){
				return response;
			}
		}
		return (Type)((Node)ast.root).accept(verifier);
	}

	private ScopeBlock createBlock(Program ast) throws ScopeException{
		return new GlobalScope(ast.getClasses());
	}
	
	private ScopeBlock createBlock(Program ast, DeclClass library) throws ScopeException{
		return new GlobalScope(ast.getClasses(), library);
	}
	
	private ScopeBlock createBlock(DeclClass ast,boolean isHandleMain) throws ScopeException{
		return new ClassScope(ast,isHandleMain);
	}
	
	private ScopeBlock createBlock(DeclMethod ast) throws ScopeException{
		return new MethodScope(ast, ast.getFormals(), yieldLocalVariables(ast.getStatements()));
	}
	
	private ScopeBlock createBlock(StmtBlock ast) throws ScopeException{
		return new StmtBlockScope(yieldLocalVariables(ast.getStatements())); 
	}
	
	private ScopeBlock createBlock(StmtBlock ast, boolean isOneLiner) throws ScopeException{
		return new StmtBlockScope(yieldLocalVariables(ast.getStatements()), isOneLiner); 
	}
	
	private List<LocalVariable> yieldLocalVariables(List<Statement> statements){
		List<LocalVariable> vars = new ArrayList<LocalVariable>();
		
		for (Statement s:statements){
			if (s instanceof LocalVariable){
				vars.add((LocalVariable)s);
			}
			if (s instanceof StmtIf || s instanceof StmtWhile)
			{
				vars.addAll(getVariablesFromStmt(s));
			}
		}
		return vars;
	}
		
	private List<LocalVariable> getVariablesFromStmt (Statement stmt)
	{
		List<LocalVariable> vars = new ArrayList<LocalVariable>();
		if (stmt instanceof StmtIf)
		{
			//finish recursion
			if (((StmtIf)stmt).getOperation() instanceof LocalVariable)
			{
				vars.add((LocalVariable)((StmtIf)stmt).getOperation());
			}
			else if (((StmtIf)stmt).getOperation() instanceof StmtIf)
			{
				vars.addAll(getVariablesFromStmt((StmtIf)((StmtIf)stmt).getOperation()));
			}
			else if (((StmtIf)stmt).getOperation() instanceof StmtWhile)
			{
				vars.addAll(getVariablesFromStmt((StmtWhile)((StmtIf)stmt).getOperation()));
			}
			
			if (((StmtIf)stmt).hasElse())
			{
				if (((StmtIf)stmt).getElseOperation() instanceof LocalVariable){
					vars.add((LocalVariable)((StmtIf)stmt).getElseOperation());
				}else
				{
					vars.addAll(getVariablesFromStmt(((StmtIf)stmt).getElseOperation()));
				}
			}			
		}
		
		if (stmt instanceof StmtWhile)
		{
			//finish recursion
			if (((StmtWhile)stmt).getOperation() instanceof LocalVariable)
			{
				vars.add((LocalVariable)((StmtWhile)stmt).getOperation());
			}
			else if (((StmtWhile)stmt).getOperation() instanceof StmtIf)
			{
				vars.addAll(getVariablesFromStmt((StmtIf)((StmtWhile)stmt).getOperation()));
			}
			else if (((StmtWhile)stmt).getOperation() instanceof StmtWhile)
			{
				vars.addAll(getVariablesFromStmt((StmtWhile)((StmtWhile)stmt).getOperation()));
			}
		}
		
		return vars;
	}
	
}
